/*
 * Copyright (C) 2012  Romain DUBOIS
 * 
 * This file is part of NeMoS - Network and Modular Software
 * 
 * NeMoS is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * NeMoS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with NeMoS.  If not, see <http://www.gnu.org/licenses/>.
 */
package nemos.remote.rsa;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import nemos.remote.rsa.data.ExportReferenceImpl;
import nemos.remote.rsa.data.ExportRegistrationImpl;
import nemos.remote.rsa.data.IUnregistrationListener;
import nemos.remote.rsa.data.ImportReferenceImpl;
import nemos.remote.rsa.data.ImportRegistrationImpl;
import nemos.remote.rsa.internal.RsaMessage;
import nemos.util.PropertyHelper;
import nemos.util.log.NemosLogger;

import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.osgi.framework.Bundle;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.remoteserviceadmin.EndpointDescription;
import org.osgi.service.remoteserviceadmin.ExportReference;
import org.osgi.service.remoteserviceadmin.ExportRegistration;
import org.osgi.service.remoteserviceadmin.ImportReference;
import org.osgi.service.remoteserviceadmin.ImportRegistration;
import org.osgi.service.remoteserviceadmin.RemoteServiceAdmin;
import org.osgi.service.remoteserviceadmin.RemoteServiceAdminEvent;
import org.osgi.service.remoteserviceadmin.RemoteServiceAdminListener;
import org.osgi.util.tracker.ServiceTracker;

/**
 * RemoteServiceAdminImpl : The basic {@link RemoteServiceAdmin} implementation.
 * 
 * <p>
 * This {@link RemoteServiceAdmin} implementation relies on abstract methods to
 * deal with the transport problems. It manages the OSGi mechanisms and
 * specifications (see chapter §122).
 * </p>
 * 
 * @author Romain DUBOIS
 */
@Component(componentAbstract = true)
public abstract class RemoteServiceAdminImpl implements RemoteServiceAdmin, IUnregistrationListener {

    /** The using bundle */
    private Bundle mBundle;
    /** The RSA implemented intents */
    private final Set<String> mImplementedIntents;

    /** The export references */
    private final Map<ServiceReference<?>, Collection<ExportReferenceImpl>> mExportReferences;
    /** The export registrations */
    private final Collection<ExportRegistration> mExportRegistrations;

    /** The import references */
    private final Map<EndpointDescription, ImportReferenceImpl> mImportReferences;
    /** The import registrations */
    private final Collection<ImportRegistration> mImportRegistrations;

    /** The RSA listeners tracker */
    private ServiceTracker<RemoteServiceAdminListener, RemoteServiceAdminListener> mListeners;

    /** The logger */
    private NemosLogger mLogger;

    /**
     * Constructor
     */
    public RemoteServiceAdminImpl() {
        super();

        mExportReferences = new HashMap<ServiceReference<?>, Collection<ExportReferenceImpl>>();
        mExportRegistrations = Collections
                .synchronizedCollection(new ArrayList<ExportRegistration>());

        mImportReferences = new HashMap<EndpointDescription, ImportReferenceImpl>();
        mImportRegistrations = Collections
                .synchronizedCollection(new ArrayList<ImportRegistration>());

        mImplementedIntents = new HashSet<String>();
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.osgi.service.remoteserviceadmin.RemoteServiceAdmin#exportService(org.osgi.framework.ServiceReference,
     *      java.util.Map)
     */
    @Override
    public Collection<ExportRegistration> exportService(
            @SuppressWarnings("rawtypes") final ServiceReference pReference,
            final Map<String, ?> pProperties) {

        mLogger.debug(RsaMessage.START_EXPORT, pReference.getProperty(Constants.SERVICE_PID),
                Arrays.toString(PropertyHelper.getStringArray(pReference
                        .getProperty(Constants.OBJECTCLASS))));

        // Merge properties
        final SortedMap<String, Object> lProps = new TreeMap<String, Object>(
                String.CASE_INSENSITIVE_ORDER);
        for (final String lPropKey : pReference.getPropertyKeys()) {
            lProps.put(lPropKey, pReference.getProperty(lPropKey));
        }
        if (pProperties != null) {
            lProps.putAll(pProperties);
        }
        lProps.put(Constants.OBJECTCLASS, pReference.getProperty(Constants.OBJECTCLASS));
        lProps.put(Constants.SERVICE_ID, pReference.getProperty(Constants.SERVICE_ID));

        // Handle intents
        // First merge exported intents and extra exported intents
        final Set<String> lIntents = extractStrings(lProps, Constants.SERVICE_EXPORTED_INTENTS,
                null);
        extractStrings(lProps, Constants.SERVICE_EXPORTED_INTENTS_EXTRA, lIntents);
        final Set<String> lRequiredIntents = new HashSet<String>(lIntents);
        // Then add service intents
        final Set<String> lNativeIntents = extractStrings(lProps, Constants.SERVICE_INTENTS, null);
        lIntents.addAll(lNativeIntents);
        // Finally, build exported intents
        lProps.put(Constants.SERVICE_INTENTS, lIntents.toArray(new String[lIntents.size()]));

        // Purge required intents
        lRequiredIntents.removeAll(lNativeIntents);
        lRequiredIntents.removeAll(getImplementedIntents());
        if (!lRequiredIntents.isEmpty()) {
            return Collections.emptySet();
        }

        // Select handled configuration types
        final Set<String> lConfigs = extractStrings(lProps, Constants.SERVICE_EXPORTED_CONFIGS,
                null);
        mLogger.debug(RsaMessage.DECLARED_CONFIGS, lConfigs);
        final Set<String> lDeclaredConfigs = new HashSet<String>(lConfigs);
        // If not set, pick one
        if (lConfigs.isEmpty()) {
            if (!getConfigurations().isEmpty()) {
                lConfigs.add(getConfigurations().get(0));
            }
        }
        // Otherwise, retain only the known and eligible configurations
        else {
            lConfigs.retainAll(getConfigurations());
        }

        // If no config found, do not go further
        mLogger.debug(RsaMessage.RETAINED_CONFIGS, lConfigs);
        if (lConfigs.isEmpty()) {
            return Collections.emptySet();
        }

        // Prepare interfaces
        final Set<String> lIntfs = extractStrings(lProps, Constants.SERVICE_EXPORTED_INTERFACES,
                null);
        mLogger.debug(RsaMessage.DECLARED_INTERFACES, lIntfs);
        if (lIntfs.size() == 1 && "*".equals(lIntfs.iterator().next())) { //$NON-NLS-1$
            lIntfs.clear();
            for (final String lInterface : extractStrings(lProps, Constants.OBJECTCLASS, null)) {
                try {
                    final Class<?> lClass = pReference.getBundle().loadClass(lInterface);
                    if (lClass.isInterface()) {
                        lIntfs.add(lInterface);
                    }
                } catch (final ClassNotFoundException e) {
                    mLogger.warn(pReference, RsaMessage.UNKNOWN_CLASS, e, lInterface, pReference
                            .getBundle().getSymbolicName());
                }
            }
        }
        mLogger.debug(RsaMessage.RETAINED_INTERFACES, lIntfs);

        // No valid interface : do not export
        if (lIntfs.isEmpty()) {
            return Collections.emptySet();
        }
        lProps.put(Constants.SERVICE_EXPORTED_INTERFACES, lIntfs.toArray(new String[lIntfs.size()]));

        // Prepare registration list
        final Collection<ExportRegistration> lRegistrations = new ArrayList<ExportRegistration>(
                lConfigs.size());
        synchronized (mExportReferences) {
            Collection<ExportReferenceImpl> lExports = mExportReferences.get(pReference);

            // Init export references
            if (lExports == null) {
                lExports = new ArrayList<ExportReferenceImpl>();
                mExportReferences.put(pReference, lExports);
            }
            // Service has already been exported
            else {
                // Try to use the previous references
                for (final Iterator<ExportReferenceImpl> lIt = lExports.iterator(); lIt.hasNext();) {

                    final ExportRegistrationImpl lRegistration = lIt.next().openRegistration();

                    // Can be null if service is no longer exported
                    if (lRegistration == null) {
                        lIt.remove();
                    } else {
                        lRegistrations.add(lRegistration);
                        mLogger.debug(RsaMessage.EXISTING_REFERENCE);
                    }
                }
            }

            // Try to create an Endpoint for each configuration type
            if (lRegistrations.isEmpty()) {
                for (final String lConfiguration : lConfigs) {
                    mLogger.debug(RsaMessage.EXPORT_WITH_CONFIG, lConfiguration);

                    ExportRegistration lRegistration = null;
                    try {
                        // Clean properties of other configuration parameters
                        final Map<String, Object> lConfigProps = new TreeMap<String, Object>(lProps);
                        for (final String lOtherConfig : lDeclaredConfigs) {
                            if (!lOtherConfig.equals(lConfiguration)) {
                                for (final Iterator<String> lKeyIt = lConfigProps.keySet()
                                        .iterator(); lKeyIt.hasNext();) {
                                    final String lKey = lKeyIt.next().toLowerCase();
                                    if (lKey.startsWith(lOtherConfig.toLowerCase())) {
                                        lKeyIt.remove();
                                    }
                                }
                            }
                        }
                        lConfigProps.put(Constants.SERVICE_IMPORTED_CONFIGS,
                                new String[] { lConfiguration });
                        final EndpointDescription lEndpoint = export(pReference, lConfigProps);
                        mLogger.debug(RsaMessage.ENDPOINT_CREATED, lEndpoint.getId());

                        final ExportReferenceImpl lExportRef = new ExportReferenceImpl(pReference,
                                lEndpoint, this);
                        lExports.add(lExportRef);

                        // Build registration
                        lRegistration = lExportRef.openRegistration();

                    } catch (final Exception e) {
                        // Create an invalid registration
                        lRegistration = new ExportRegistrationImpl(e);
                    }

                    // Keep valid export registrations
                    if (lRegistration.getException() == null) {
                        mExportRegistrations.add(lRegistration);
                    }
                    lRegistrations.add(lRegistration);
                }
            }
        }

        // Notify RemoteServiceAdminListeners
        mLogger.debug(RsaMessage.NOTIFY_EXPORT);
        for (final ExportRegistration lRegistration : lRegistrations) {
            final RemoteServiceAdminEvent lEvent;
            if (lRegistration.getException() == null) {
                lEvent = new RemoteServiceAdminEvent(RemoteServiceAdminEvent.EXPORT_REGISTRATION,
                        mBundle, lRegistration.getExportReference(), null);
            } else {
                lEvent = new RemoteServiceAdminEvent(RemoteServiceAdminEvent.EXPORT_ERROR, mBundle,
                        (ExportReference) null, lRegistration.getException());
            }
            notifyListeners(lEvent);
        }

        mLogger.debug(RsaMessage.EXPORT_ENDED);
        return lRegistrations;
    }

    /**
     * {@inheritDoc}
     * 
     * @see nemos.remote.rsa.data.IUnregistrationListener#exportUnregistered(org.osgi.service.remoteserviceadmin.ExportReference)
     */
    @Override
    public void exportUnregistered(final ExportReference pReference) {
        notifyListeners(new RemoteServiceAdminEvent(RemoteServiceAdminEvent.EXPORT_UNREGISTRATION,
                mBundle, pReference, null));
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.osgi.service.remoteserviceadmin.RemoteServiceAdmin#getExportedServices()
     */
    @Override
    public Collection<ExportReference> getExportedServices() {

        final Collection<ExportReference> lReferences = new ArrayList<ExportReference>();
        synchronized (mExportReferences) {
            for (final Iterator<Collection<ExportReferenceImpl>> lIt = mExportReferences.values()
                    .iterator(); lIt.hasNext();) {

                final Collection<ExportReferenceImpl> lRefs = lIt.next();
                for (final Iterator<ExportReferenceImpl> lRefIt = lRefs.iterator(); lRefIt
                        .hasNext();) {

                    final ExportReferenceImpl lReference = lRefIt.next();
                    if (lReference.isActive()) {
                        lReferences.add(lReference);
                    } else {
                        // Remove useless references
                        lRefIt.remove();
                    }
                }

                // Remove useless services
                if (lRefs.isEmpty()) {
                    lIt.remove();
                }
            }
        }
        return lReferences;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.osgi.service.remoteserviceadmin.RemoteServiceAdmin#getImportedEndpoints()
     */
    @Override
    public Collection<ImportReference> getImportedEndpoints() {
        final Collection<ImportReference> lReferences = new ArrayList<ImportReference>();
        synchronized (mImportReferences) {
            for (final Iterator<ImportReferenceImpl> lRefIt = mImportReferences.values().iterator(); lRefIt
                    .hasNext();) {

                final ImportReferenceImpl lReference = lRefIt.next();
                if (lReference.isActive()) {
                    lReferences.add(lReference);
                } else {
                    // Remove useless references
                    lRefIt.remove();
                }
            }
        }
        return lReferences;
    }

    /**
     * {@inheritDoc}
     * 
     * @see org.osgi.service.remoteserviceadmin.RemoteServiceAdmin#importService(org.osgi.service.remoteserviceadmin.EndpointDescription)
     */
    @Override
    public ImportRegistration importService(final EndpointDescription pEndpoint) {
        mLogger.debug(RsaMessage.START_IMPORT, pEndpoint.getId(), pEndpoint.getFrameworkUUID(),
                Long.valueOf(pEndpoint.getServiceId()), pEndpoint.getInterfaces());

        ImportRegistration lRegistration = null;

        // Select a valid configuration type
        String lType = null;
        for (final String lConfigType : pEndpoint.getConfigurationTypes()) {
            if (getConfigurations().contains(lConfigType)) {
                lType = lConfigType;
                break;
            }
        }
        mLogger.debug(RsaMessage.IMPORT_FOR_CONFIG, lType);

        // At least one configuration type is handled
        if (lType != null) {

            synchronized (mImportReferences) {

                try {
                    // Check the endpoint is not already imported
                    ImportReferenceImpl lImportRef = mImportReferences.get(pEndpoint);
                    if (lImportRef != null) {
                        lRegistration = lImportRef.openRegistration();

                        if (lRegistration == null) {
                            mImportReferences.remove(pEndpoint);
                        } else {
                            mLogger.debug(RsaMessage.EXISTING_REFERENCE);
                        }
                    }

                    // New Import
                    if (lRegistration == null) {

                        // Build reference and registration
                        final ServiceRegistration<?> lServiceReg = importEndpoint(pEndpoint, lType);
                        if (lServiceReg != null) {
                            lImportRef = new ImportReferenceImpl(lServiceReg, pEndpoint, this);
                            mImportReferences.put(pEndpoint, lImportRef);
                            lRegistration = lImportRef.openRegistration();
                        }
                    }

                } catch (final Exception e) {
                    // Create an invalid registration
                    lRegistration = new ImportRegistrationImpl(e);
                }
            }

            if (lRegistration != null) {
                // Keep valid import registrations
                if (lRegistration.getException() == null) {
                    mImportRegistrations.add(lRegistration);
                }

                // Notify RemoteServiceAdminListeners
                mLogger.debug(RsaMessage.NOTIFY_IMPORT);
                final RemoteServiceAdminEvent lEvent;
                if (lRegistration.getException() == null) {
                    lEvent = new RemoteServiceAdminEvent(
                            RemoteServiceAdminEvent.IMPORT_REGISTRATION, mBundle,
                            lRegistration.getImportReference(), null);
                } else {
                    lEvent = new RemoteServiceAdminEvent(RemoteServiceAdminEvent.IMPORT_ERROR,
                            mBundle, (ImportReference) null, lRegistration.getException());
                }
                notifyListeners(lEvent);
            }
        }

        mLogger.debug(RsaMessage.IMPORT_ENDED);
        return lRegistration;
    }

    /**
     * {@inheritDoc}
     * 
     * @see nemos.remote.rsa.data.IUnregistrationListener#importUnregistered(org.osgi.service.remoteserviceadmin.ImportReference)
     */
    @Override
    public void importUnregistered(final ImportReference pReference) {
        notifyListeners(new RemoteServiceAdminEvent(RemoteServiceAdminEvent.IMPORT_UNREGISTRATION,
                mBundle, pReference, null));
    }

    /**
     * Start the service.
     * 
     * @param pReference
     *            The service reference
     */
    @Activate
    public void start(final ServiceReference<RemoteServiceAdmin> pReference) {
        mLogger = new NemosLogger(RemoteServiceAdminImpl.class);

        // Collect implemented intents
        mImplementedIntents.clear();
        final Object lRawIntents = pReference.getProperty(Constants.SERVICE_INTENTS);
        if (lRawIntents != null) {
            for (final String lIntent : PropertyHelper.getStringArray(lRawIntents)) {
                mImplementedIntents.add(lIntent);
            }
        }

        mListeners = new ServiceTracker<RemoteServiceAdminListener, RemoteServiceAdminListener>(
                mBundle.getBundleContext(), RemoteServiceAdminListener.class, null);
        mListeners.open();
    }

    /**
     * Set the using bundle.
     * 
     * @param pBundle
     *            The using bundle
     */
    public void setBundle(final Bundle pBundle) {
        mBundle = pBundle;
    }

    /**
     * Stop the component.
     */
    @Deactivate
    public void stop() {
        // Clean export registrations
        synchronized (mExportRegistrations) {
            for (final ExportRegistration lRegistration : mExportRegistrations) {
                lRegistration.close();
            }
            mExportRegistrations.clear();
        }
        // Purge the export map
        getExportedServices();

        // Clean import registrations
        synchronized (mImportRegistrations) {
            for (final ImportRegistration lRegistration : mImportRegistrations) {
                lRegistration.close();
            }
            mImportRegistrations.clear();
        }
        // Purge the import map
        getImportedEndpoints();

        // Close tracker
        if (mBundle.getState() == Bundle.ACTIVE) {
            mListeners.close();
        }
    }

    /**
     * Create an endpoint from the service merged properties.
     * 
     * @param pReference
     *            The service reference to export
     * @param pProperties
     *            The merged and cleaned properties
     * @return The export endpoint
     * @throws Exception
     *             If the export failed
     */
    protected abstract EndpointDescription export(ServiceReference<?> pReference,
            final Map<String, Object> pProperties) throws Exception;

    /**
     * Get the using bundle.
     * 
     * @return The using bundle
     */
    protected Bundle getBundle() {
        return mBundle;
    }

    /**
     * Get the supported configurations.
     * 
     * @return The supported configurations.
     */
    protected abstract List<String> getConfigurations();

    /**
     * Get the implemented intents.
     * 
     * @return The implemented intents
     */
    protected Set<String> getImplementedIntents() {
        return mImplementedIntents;
    }

    /**
     * Import an endpoint in the local framework.
     * 
     * @param pEndpoint
     *            The endpoint to import
     * @param pType
     *            The selected configuration type
     * @return The import reference
     */
    protected abstract ServiceRegistration<?> importEndpoint(final EndpointDescription pEndpoint,
            final String pType);

    /**
     * Notify {@link RemoteServiceAdminListener}s of a new event.
     * 
     * @param pEvent
     *            The event to broadcast
     */
    protected void notifyListeners(final RemoteServiceAdminEvent pEvent) {
        final Object[] lServices = mListeners.getServices();
        if (lServices != null) {
            for (final Object lListener : lServices) {
                ((RemoteServiceAdminListener) lListener).remoteAdminEvent(pEvent);
            }
        }
    }

    /**
     * Extract intents from a property map.
     * 
     * @param pProps
     *            The properties
     * @param pPropertyName
     *            The intents property name
     * @param pIntents
     *            The intents to fill
     * @return The intents
     */
    private Set<String> extractStrings(final Map<?, ?> pProps, final String pPropertyName,
            final Set<String> pIntents) {

        // Prepare result
        final Set<String> lIntents;
        if (pIntents == null) {
            lIntents = new HashSet<String>();
        } else {
            lIntents = pIntents;
        }

        // Extract property
        final Object lExportIntents = pProps.get(pPropertyName);
        if (lExportIntents != null) {
            for (final String lIntent : PropertyHelper.getStringArray(lExportIntents)) {
                lIntents.add(lIntent);
            }
        }
        return lIntents;
    }
}
